/**
 *Copyright [2010] [lcj(caijunlu@gmail.com)]
 *Licensed under the Apache License, Version 2.0 (the "License");
 *you may not use this file except in compliance with the License. 
 *You may obtain a copy of the License at 
 *             http://www.apache.org/licenses/LICENSE-2.0 
 *Unless required by applicable law or agreed to in writing, 
 *software distributed under the License is distributed on an "AS IS" BASIS, 
 *WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 
 *either express or implied. See the License for the specific language governing permissions and limitations under the License
 */
package org.independent.servlet.session.manager;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import net.rubyeye.xmemcached.transcoders.Transcoder;

import org.independent.servlet.session.ServletSession;
import org.independent.servlet.session.exception.ServletSessionException;
import org.independent.servlet.session.service.ContextHolderFactory;
import org.independent.servlet.session.util.StringUtil;
import org.independent.servlet.session.util.XMemcachedClient;
import org.independent.servlet.session.util.thread.ThreadPool;
import org.independent.servlet.session.util.thread.ThreadPoolTask;

/**
 * httpSession manager and the system configuration
 * 
 * @author lcj
 * 
 */
public class SessionsManager {

	/**
	 * the number of sessions be managed on local JVM is limited, the maximum
	 * value is configurable
	 */
	static private Map<String, ServletSession> sessionMapping; // local session

	static private int localSessionsSize = 50; // local sessions max number

	/**
	 * this need configuration use Xmemcached as the memcache's client cached
	 * the XMemcachedClient
	 */
	public static XMemcachedClient xMemcachedClient;

	/**
	 * the SessionIdManager need configuration
	 */
	private static SessionIdManager sessionIdManager;

	/**
	 * the isSSO need configuration 0- not use single sign on 1-need use sigle
	 * sign on
	 */
	private int isSSO = 0;

	private final static int sessionTimeUnit = 60000;

	/**
	 * mark the local sessions Whether has max number while 0 is no max ; while
	 * 1 be limited max value
	 */
	private int markSessionMaxNumber = 1;

	/**
	 * session out time, is configurable
	 */
	public static long sessionTime = 0;

	/**
	 * the identify of web server is set in the property file used this to
	 * identify the session's id on memcached
	 */
	private static String serverId = "";

	/**
	 * the name of session's id
	 */
	public static String sessionIdName = "JSessionID";

	private static ArrayList<SessionSecurity> sessionSecurityChecks = new ArrayList<SessionSecurity>();

	/**
	 * while true mean all the client commit the session's id must be check for
	 * security while false mean not
	 */
	private static boolean needIdCheck = false;

	/**
	 * the remote ip be bonding on the session ,this is the Name of attribute in
	 * session
	 */
	final static public String SESSIONIDMARK = "sessionIdCheckMark";

	/**
	 * This is the session's Id prepared for remove from the
	 * LocalSessionsManager
	 */
	private static String sessionIdForRemove = "";

	private static SessionsManager localSessionsManager = null;

	/**
	 * Get a single object of LocalSessionsManager in the jvm
	 */
	public static SessionsManager getInstance() {
		synchronized (SessionsManager.class) {
			if (localSessionsManager == null) {
				localSessionsManager = new SessionsManager();
			}
		}
		return localSessionsManager;
	}

	/**
	 * Initialization the LocalSessionsManager
	 */
	@SuppressWarnings("unchecked")
	private SessionsManager() {
		ClassLoader cl = Thread.currentThread().getContextClassLoader();
		InputStream infile = cl.getResourceAsStream("ishm.properties");
		Properties props = new Properties();
		try {
			props.load(infile);
			sessionTime = Integer.parseInt(props.getProperty(
					"session.out.time", "30")) * sessionTimeUnit;
			localSessionsSize = Integer.parseInt(props.getProperty(
					"local.sessions.capacity", "100"));
			sessionMapping = localSessionsSize == 0 ? new HashMap<String, ServletSession>()
					: new HashMap<String, ServletSession>(localSessionsSize);
			serverId = props.getProperty("server.id", "test");
			sessionIdManager = new SessionIdManager(sessionTime, serverId);
			sessionIdName = props.getProperty("sessionIdName", "JSessionID");
			String transcoderType = props.getProperty("transcoder.type.name",
					"org.independent.servlet.session.util.kyro.KryoTranscoder");
			xMemcachedClient = XMemcachedClient.getInstance(
					props.getProperty("memcached.server.list", null),
					Integer.parseInt(props.getProperty(
							"memcashed.session.out.time", "60"))
							* sessionTimeUnit, (Transcoder<Object>) (Class
							.forName(transcoderType).newInstance()));
			needIdCheck = props.getProperty("session.securety.check", "false")
					.equalsIgnoreCase("true") ? true : false;
			if (needIdCheck) {
				String[] checks = props.getProperty(
						"session.security.checkers", null).split(";");
				for (int i = 0; i < checks.length; i++) {
					sessionSecurityChecks.add((SessionSecurity) Class.forName(
							checks[i]).newInstance());
				}
			}
			markSessionMaxNumber = localSessionsSize == 0 ? 0 : 1;
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public int getIsSSO() {
		return isSSO;
	}

	/**
	 * get session by the request if the request has the session's id ,get the
	 * session by id ,else if the request has no session's id create a new
	 * session to return
	 * 
	 * @param request
	 * @return httpsession
	 */
	public HttpSession getSessionByRequest(HttpServletRequest request) {
		ServletSession session = null;
		String sessionId = sessionIdManager.getSessionIdRequest(request);

		if (ContextHolderFactory.getServiceContextHolder().getContext()
				.getHasSession() == 0
				&& StringUtil.isBlank(sessionId)) {
			session = createNewSession();
		} else if (!StringUtil.isBlank(sessionId) && getIsSSO() == 0) {
			session = getSessionById(sessionId);
		} else {
			// TODO Single sign on
			// session = getSessionById(sessionId, o);
		}

		if (needIdCheck)
			for (SessionSecurity s : sessionSecurityChecks) {
				s.checkSessionByRequest(request, session);
			}
		ContextHolderFactory.getServiceContextHolder().getContext()
				.setSession(session);
		ContextHolderFactory.getServiceContextHolder().getContext()
				.setHasSession(1);
		return session;
	}

	/**
	 * before the session's id to be send to client ,the id will be add cru time
	 * {@link org.independent.servlet.session.manager.SessionIdManager.formateSessionId}
	 */
	public String formatSessionId(String sessionId) {
		return sessionIdManager.formateSessionId(sessionId);
	}

	/**
	 * if the configuration  for check the session's id next time be submitted by client
	 * whether right ,if need to check ,as the session is created time ,can also
	 * store the remote client ip for next to check
	 * 
	 * @return a new ServletSession
	 */
	public ServletSession createNewSession() {
		if (sessionIdManager == null) {
			throw new ServletSessionException("SessionIdManager is null!!");
		}
		String sessionId = sessionIdManager.createSessionId();
		if (StringUtil.isBlank(sessionId)) {
			throw new ServletSessionException(
					" when the session's id is null,new session can't be create!!");
		}
		if (sessionId.indexOf("@") != -1) {
			throw new ServletSessionException(
					" when the session's id is null,new session can't be create!!");
		}
		ServletSession session = new ServletSession();
		session.setSessionId(sessionId);
		session.setCreationTime(System.currentTimeMillis());
		session.setLastMemcachTime(System.currentTimeMillis());
		if (needIdCheck) {
			session.setAttribute(SESSIONIDMARK, ContextHolderFactory
					.getServiceContextHolder().getContext().getSessionMark());
		}
		storeSession(session);
		return session;
	}

	/**
	 * use the id to get session from the localSessionsMamager ,if there is no
	 * the session in the localSessionsMamager,then get the session from
	 * memcached
	 * 
	 * @param sessionId
	 * 
	 * @return
	 */
	public ServletSession getSessionById(String sessionId) {
		if (sessionId.indexOf("@") != -1)
			sessionId = sessionIdManager.checkSessionTime(sessionId);
		ServletSession session = sessionMapping.get(sessionId);
		try {
			session = session == null ? (ServletSession) xMemcachedClient
					.defaultGet(sessionId) : session;
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		session.setLastAccessedTime(System.currentTimeMillis());
		if (System.currentTimeMillis() > (session.getLastMemcachTime() + sessionTime)) {
			storeSession(session);
		}
		return session;
	}

	/**
	 * save the session ; If the max size of sessionMapping is set ,before store
	 * the session,it will check the sessionMapping has the sessions numbers, if
	 * the size close the max size ,the sessionMapping will be removed a old
	 * session for put the new session
	 * 
	 * @param session
	 */
	public void storeSession(ServletSession session) {
		if ((localSessionsSize - sessionMapping.size() < 10)
				&& markSessionMaxNumber == 1) {
			cleanUpLocalSessions();
		}
		putIntoMapping(session);
		 remoteStore(session);
		sessionIdForRemove = session.getId();
	}

	/**
	 * cleanup the localSessionsMamager,for a new session to put in
	 */
	private void cleanUpLocalSessions() {
		cleanSession();
		sessionMapping.remove(sessionIdForRemove);
	}

	/**
	 * store the session on the sessionMapping of local
	 * 
	 * @param session
	 */
	private void putIntoMapping(ServletSession session) {
		session.setLastAccessedTime(System.currentTimeMillis());
		sessionMapping.put(session.getId(), session);
	}

	/**
	 * store the session on the remote memcached
	 * 
	 * @param session
	 */
	private void remoteStore(ServletSession session) {
		session.setLastMemcachTime(System.currentTimeMillis());
		ThreadPool.doRun(new ThreadPoolTask(session, xMemcachedClient));
	}

	/*
	 * Regular clean-up sessions,then the sessions have expired
	 */
	public void cleanSession() {
		Iterator<String> it = sessionMapping.keySet().iterator();
		while (it.hasNext()) {
			String key = it.next();
			ServletSession session = (ServletSession) sessionMapping.get(key);
			if (System.currentTimeMillis() - session.getLastAccessedTime() > sessionTime) {
				sessionMapping.remove(key);
				xMemcachedClient.delete(session.getId());
			}
		}
	}
}
